/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkAssignStmt, ArkInstanceFieldRef, ArkMethod, ArrayType, ClassSignature, Constant, Local, Stmt, StringType, Value, ArkField } from 'arkanalyzer/lib';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { Rule, Defects, MethodMatcher, MatcherTypes, MatcherCallback, CheckerUtils } from '../../Index';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'WebResponseDataTypeCheck');

const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/web-response-data-type-check.md',
    description: 'Avoid using string type data in setResponseData method. Use ArrayBuffer type for better performance.'
};

// 字符串常量定义
const SET_RESPONSE_DATA_METHOD = 'setResponseData';
const THIS_KEYWORD = 'this';

export class WebResponseDataTypeCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private methodMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD
    };

    public registerMatchers(): MatcherCallback[] {
        return [{
            matcher: this.methodMatcher,
            callback: this.check
        }];
    }

    public check = (targetMethod: ArkMethod): void => {
        const stmts = targetMethod.getBody()?.getCfg().getStmts() ?? [];

        for (let stmt of stmts) {
            this.processStatement(stmt);
        }
    };

    private processStatement(stmt: Stmt): void {
        const invoker = CheckerUtils.getInvokeExprFromStmt(stmt);
        if (!invoker || invoker.getMethodSignature().getMethodSubSignature().getMethodName() !== SET_RESPONSE_DATA_METHOD) {
            return;
        }
        const args = invoker.getArgs();
        if (args.length === 0) {
            return;
        }
        // 记录根参数stmt和value
        this.processSetResponseDataArg(stmt, args[0], stmt, args[0]);
    }

    private processSetResponseDataArg(stmt: Stmt, arg: Value, rootStmt: Stmt, rootValue: Value): void {
        if (arg instanceof Constant && typeof arg.getValue() === 'string') {
            this.addIssueReport(rootStmt, rootValue);
        } else if (arg instanceof Local) {
            this.processLocalVariableType(stmt, arg, rootStmt, rootValue);
        } else if (arg instanceof ArkInstanceFieldRef) {
            this.processInstanceFieldType(stmt, arg, rootStmt, rootValue);
        }
    }

    private processLocalVariableType(stmt: Stmt, local: Local, rootStmt: Stmt, rootValue: Value): void {
        const localType = local.getType();
        if (localType instanceof StringType) {
            this.addIssueReport(rootStmt, rootValue);
        } else if (localType instanceof ArrayType) {
            return;
        } else {
            this.processVariableAssignment(stmt, local, rootStmt, rootValue);
        }
    }

    private processInstanceFieldType(stmt: Stmt, fieldRef: ArkInstanceFieldRef, rootStmt: Stmt, rootValue: Value): void {
        const base = fieldRef.getBase();
        if (base.getName() !== THIS_KEYWORD) {
            return;
        }
        const fieldSignature = fieldRef.getFieldSignature();
        const declaringSignature = fieldSignature.getDeclaringSignature();
        if (!(declaringSignature instanceof ClassSignature)) {
            return;
        }
        const className = declaringSignature.getClassName();
        const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
        const arkClass = arkFile.getClassWithName(className);
        if (!arkClass) {
            return;
        }
        const fieldName = fieldRef.getFieldName();
        const arkField = arkClass.getFieldWithName(fieldName);
        if (!arkField) {
            return;
        }
        const fieldType = arkField.getType();
        if (fieldType instanceof StringType) {
            this.addIssueReport(rootStmt, rootValue);
        } else if (fieldType instanceof ArrayType) {
            return;
        } else {
            this.processFieldInitialization(stmt, arkField, rootStmt, rootValue);
        }
    }

    private processVariableAssignment(stmt: Stmt, local: Local, rootStmt: Stmt, rootValue: Value): void {
        const declaringStmt = local.getDeclaringStmt();
        if (!(declaringStmt instanceof ArkAssignStmt)) {
            return;
        }
        const rightOp = declaringStmt.getRightOp();
        if (rightOp instanceof Constant && typeof rightOp.getValue() === 'string') {
            this.addIssueReport(rootStmt, rootValue);
        } else if (rightOp instanceof Local) {
            this.processLocalVariableType(stmt, rightOp, rootStmt, rootValue);
        } else if (rightOp instanceof ArkInstanceFieldRef) {
            this.processInstanceFieldType(stmt, rightOp, rootStmt, rootValue);
        }
    }

    private processFieldInitialization(stmt: Stmt, arkField: ArkField, rootStmt: Stmt, rootValue: Value): void {
        const initializerStmts = arkField.getInitializer();
        if (initializerStmts.length === 0) {
            return;
        }
        const initStmt = initializerStmts[0];
        if (!(initStmt instanceof ArkAssignStmt)) {
            return;
        }
        const rightOp = initStmt.getRightOp();
        if (rightOp instanceof Constant && typeof rightOp.getValue() === 'string') {
            this.addIssueReport(rootStmt, rootValue);
        } else if (rightOp instanceof Local) {
            this.processLocalVariableType(stmt, rightOp, rootStmt, rootValue);
        } else if (rightOp instanceof ArkInstanceFieldRef) {
            this.processInstanceFieldType(stmt, rightOp, rootStmt, rootValue);
        }
    }

    /**
     * 报告问题
     */
    private addIssueReport(stmt: Stmt, rightOp: Value): void {
        const reportPosition = stmt.getOperandOriginalPosition(rightOp)!;
        const filePath = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getFilePath();
        const severity = this.rule.alert ?? this.metaData.severity;
        const line = reportPosition.getFirstLine();
        const startCol = reportPosition.getFirstCol();
        let endCol = reportPosition.getLastCol();
        if (endCol > startCol) {
            endCol--;
        }
        let defects = new Defects(line, startCol, endCol, this.metaData.description, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, true);
        this.issues.push(new IssueReport(defects, undefined));
    }
}
